#define UNMANAGED_EXPORTS

#include "test_manager.h"

/** Vytvori manazera testu a otevre soubor se zadanim problemu.
 * \param configuration jmeno vstupniho souboru
 * \param displayer zobrazovac informaci o prubehu vypoctu
 */
TestManager::TestManager(const char * configuration, ProgressDisplayer * displayer) {
	// set the rand seed for the whole program
	srand((unsigned int)time(NULL));
	out_ = &std::cout;
	log_ = &std::clog;
	error = false;
	// load the XML
	try {
		document.LoadFile(configuration);
		ticpp::Element * settings = document.FirstChildElement("settings", false);
		if (settings) {
			OpenFile(&out_, settings->GetAttribute("output"));
			OpenFile(&log_, settings->GetAttribute("log"));
		}
		*out_ << "<?xml version=\"1.0\" ?>" << std::endl;
		base = new Options(log_);
		if (displayer != NULL) {
			base->SetDisplayObject(displayer);
		}
		base->SetDisplayAvailability(out_ != &std::cout && log_ != &std::clog);
	}
	catch (ticpp::Exception & ex) {
		error = true;
		std::cout << ex.what() << std::endl;
	}
	catch (...) {
		error = true;
		std::cout << "UNKNOWN exception caught while opening the settings file!";
	}
}

/** Zavre otevrene vystupni soubory a odalokuje pouzitou pamet.
 */
TestManager::~TestManager()  {
	if (!error) delete base;
	if (out_ != &std::cout) {
		((std::ofstream*)out_)->close();
		delete out_;
	}
	if (log_ != &std::clog) {
		((std::ofstream*)log_)->close();
		delete log_;
	}
}

/** Spocita pocet algoritmu, ktere budou spusteny, ve vstupnim souboru.
 * \return celkovy pocet algoritmu
 */
int TestManager::CountTests() {
	int count = 0;
	ticpp::Iterator<ticpp::Element> test("test");
	ticpp::Iterator<ticpp::Element> algorithmConfig;
	for (test = test.begin(document.GetDocument(false)); test != test.end(); ++test) {
		ticpp::Element * algorithmBase = test->FirstChildElement("algorithm", false);
		if (!algorithmBase) {
			continue;
		}
		// load and run all the algorithms
		for (algorithmConfig = algorithmConfig.begin(algorithmBase); algorithmConfig != algorithmConfig.end(); ++algorithmConfig) {
			++count;
		}
	}
	return count;
}

/** Vyresi vsechny testy v zadani. Pro kazdy test vytvori jednu mapu, te necha nacist nastaveni,
 * pro kazdy algoritmus, kterym ma byt zadana mapa vyresena, vytvori prislusny objekt algoritmu,
 * necha mu nacist zadani, prideli mu drive vytvorenou mapu a necha ho spocitat reseni. Vysledek
 * ulozi do historie.
 */
void TestManager::SolveTests() {
	if (error) {
		return;
	}
	round = 0;
	base->TestCount(CountTests());

	ticpp::Iterator<ticpp::Element> test("test");
	for (test = test.begin(document.GetDocument(false)); test != test.end(); ++test) {
		std::ostream * gout = NULL, * glog = NULL;
		std::string outName = test->GetAttribute("output");
		std::string logName = test->GetAttribute("log");
		std::string testName;
		(test.Get())->GetAttributeOrDefault("name", &testName, IntToStr(++round) + ". problem");
		PrintTestHeader(testName);
		if (!outName.empty()) {
			gout = out_;
			if (!OpenFile(&out_, outName)) {
				gout = NULL;
			}
			else {
				PrintTestOutputHeader(testName);
				*gout << "	<file>" << outName << "</file>" << std::endl << "</test>" << std::endl;
			}
		}
		if (!logName.empty()) {
			glog = log_;
			if (!OpenFile(&log_, logName)) {
				glog = NULL;
			}
			else {
				PrintTestLogHeader(testName);
				*glog << ">>> Log is in file " << outName << std::endl;
			}
		}
		base->SetLog(log_);
		// load the area
		Area * area = GetArea(test->FirstChildElement("map", false), base);
		if (!area) {
			*out_ << "	<error>Invalid map settings.</error>" << std::endl;
			CloseFiles(gout, glog);
			continue;
		}
		ticpp::Element * algorithmBase = test->FirstChildElement("algorithm", false);
		if (!algorithmBase) {
			*out_ << "	<error>Missing 'algorithm' element.</error>" << std::endl;
			delete area;
			CloseFiles(gout, glog);
			continue;
		}
		PrintAreaHeader(area);

		// load and run all the algorithms
		ticpp::Iterator<ticpp::Element> algorithmConfig;
		for (algorithmConfig = algorithmConfig.begin(algorithmBase); algorithmConfig != algorithmConfig.end(); ++algorithmConfig) {
			Algorithm * algorithm = GetAlgorithm(algorithmConfig.Get(), base);
			if (!algorithm) {
				*out_ << "	<solution>" << std::endl << "		<error>Invalid algorithm settings (" << algorithmConfig->Value() << ").</error>" << std::endl << "	</solution>" << std::endl;
				base->NextTest(false);
				continue;
			}
			// run the algorithm for the given area
			algorithm->Set(area);
			if (algorithm->AllConfigurations()) {
				while (algorithm->Next()) {
					PrintAlgorithmHeader(algorithm);
					algorithm->Solve();
					SaveResults(algorithm);
				}
			}
			else {
				PrintAlgorithmHeader(algorithm);
				algorithm->Solve();
				SaveResults(algorithm);
			}
			area->clear();
			delete algorithm;
		}
		delete area;
		CloseFiles(gout, glog);
	}
}

/** Otevre soubor daneho jmena.
 * \param file vystupni stream, ktery ma zapisovat do daneho souboru, vystupni parametr
 * \param name jmeno souboru
 * \return true, pokud se soubor povedlo otevrit pro zapis
 */
bool TestManager::OpenFile(std::ostream ** file, const std::string & name) {
	if (name != "") {
		std::ofstream * f;
		f = new std::ofstream(name.c_str());
		if (f->good()) {
			*file = f;
			return true;
		}
		else {
			delete f;
			return false;
		}
	}
	return false;
}

/** Zavre vystupni soubor a vystupni logovaci soubor, pokud byly drive otevreny.
 * \param gout vystupni stream
 * \param glog vystupni logovaci stream
 */
void TestManager::CloseFiles(std::ostream * gout, std::ostream * glog) {
	// finish the test
	*out_ << "</test>" << std::endl;
	// close the files for the current test and return the values to the global ones
	if (gout != NULL) {
		((std::ofstream *)out_)->close();
		delete out_;
		out_ = gout;
	}
	if (glog != NULL) {
		((std::ofstream *)log_)->close();
		delete log_;
		log_ = glog;
	}
}

/** Ulozi vysledky spoctene algoritmem. Vytvorene prirazeni overi a  do vystupniho souboru zapise vysledek.
 * \param al algoritmus, ktery prave dopocital reseni problemu
 */
void TestManager::SaveResults(Algorithm * al) {
	*out_ << "	<solution algorithm=\"" << al->GetDetail() << "\">" << std::endl;
	double time;
	if (al->Correct()) {
		time = history.push(al->Frequencies(), al->GetDetail(), al->AreaDetail());
	}
	else {
		time = history.push(al->Frequencies(), "!!!" + al->GetDetail(), al->AreaDetail());
		*out_ << "		<error>Bad assignment.</error>" << std::endl;
		*log_ << "!!! BAD ASSIGNMENT !!!" << std::endl;
	}
	al->PrintResult(*out_);
	*out_ << "		<time>" << time << "</time>" << std::endl;
	*out_ << "	</solution>" << std::endl;
}

/** Vytiskne tabulku dosazenych vysledku.
 */
void TestManager::PrintResults() {
	if (error) {
		std::cout << "Error: could not load and parse the input file!";
	}
	else {
		*log_ << history;
		if (*out_ != std::cout) {
			std::cout << history;
		}
	}
}

/** Do vystupniho XML souboru vypise hlavicku noveho testu.
 * \param testName nazev testu
 */
void TestManager::PrintTestOutputHeader(const std::string & testName) {
	*out_ << "<test id=\"" << round << "\" name=\"" << testName << "\">" << std::endl;
}

/** Do vystupniho log souboru vypise hlavicku noveho testu.
 * \param testName nazev testu
 */
void TestManager::PrintTestLogHeader(const std::string & testName) {
	*log_ << "/////" << std::string(70, '-') << "\\\\\\\\\\" << std::endl
		<< "|||||" << std::string(70, ' ') << "|||||" << std::endl
		<< "|||||" << std::string(20, ' ') << std::setw(30) << testName << std::string(20, ' ') << "|||||" << std::endl
		<< "|||||" << std::string(70, ' ') << "|||||" << std::endl
		<< "\\\\\\\\\\" << std::string(70, '-') << "/////" << std::endl;
}

/** Do vystupnich souboru vypise patricne hlavicky noveho testu.
 * \param testName nazev testu
 */
void TestManager::PrintTestHeader(const std::string & testName) {
	PrintTestOutputHeader(testName);
	PrintTestLogHeader(testName);
	if (out_ != &std::cout) {
		std::cout << std::endl << "Computing solution of " << testName << ": ";
		base->NewDisplay();
	}
}

/** Vypise hlavicku nove mapy.
 * \param area nactena mapa
 */
void TestManager::PrintAreaHeader(Area * area) {
	std::string text = "@@@ MAP: " + area->getDetail();
	*out_ << "	<map>" << area->getDetail() << "</map>" << std::endl;
	*log_ << std::string(text.length() + 4, '@') << std::endl << text << " @@@" << std::endl << std::string(text.length() + 4, '@') << std::endl;
}

/** Vypise hlavicku noveho algoritmu.
 * \param algorithm algoritmus, ktery bude prave spusten
 */
void TestManager::PrintAlgorithmHeader(Algorithm * algorithm) {
	std::string text = "### ALGORITHM: " + algorithm->GetDetail();
	*log_ << std::string(text.length() + 4, '#') << std::endl << text << " ###" << std::endl << std::string(text.length() + 4, '#') << std::endl;
	base->NextTest();
}

/** Ze zadaneho XML elementu reprezentujiciho algoritmus vytvori prislusnou tridu algoritmu a nacte mu nastaveni.
 * \param algorithmNode XML element algorithm obsahujici nastaveni vytvareneho algoritmu
 * \param base zakladni nastaveni programu
 * \return vytvoreny algoritmus
 */
Algorithm * TestManager::GetAlgorithm(ticpp::Element * algorithmNode, Options * base) {
	if (!algorithmNode) {
		return NULL;
	}
	std::string algorithmName = algorithmNode->Value();
	if (algorithmName.empty()) {
		return NULL;
	}
	Algorithms algorithm = algorithm_unknown;
	for (int i = 0; i < algorithm_unknown; i++) {
		if (!STRCMP(AlgorithmNames[i], algorithmName.c_str())) {
			algorithm = (Algorithms)i;
			break;
		}
	}
	Options * o;
	Algorithm * a;
	switch (algorithm) {
		case genetic: {
				o = new GeneticOptions(*base);
				o->Read(algorithmNode);
				a = new GeneticAlgorithm(o);
			}
			break;
		case permutation: {
				o = new GeneticOptions(*base);
				o->Read(algorithmNode);
				a = new PermutationAlgorithm(o);
			}
			break;
		case sequential: {
				o = new SequentialOptions(*base);
				o->Read(algorithmNode);
				a = new SequentialAlgorithm(o);
			}
			break;
		case annealing: {
				o = new AnnealingOptions(*base);
				o->Read(algorithmNode);
				a = new AnnealingAlgorithm(o);
			}
			break;
		case tabu: {
				o = new TabuOptions(*base);
				o->Read(algorithmNode);
				a = new TabuAlgorithm(o);
			}
			break;
		default:
			return NULL;
	}
	o->Print();
	return a;
}

/** Ze zadaneho XML elementu reprezentujiciho mapu vytvori prislusnou tridu mapy a nacte jeji nastaveni.
 * \param mapNode XML element map obsahujici nastaveni vytvarene mapy
 * \param base zakladni nastaveni programu
 * \return vytvorena mapa
 */
Area * TestManager::GetArea(ticpp::Element * mapNode, Options * base) {
	mapNode = mapNode->FirstChildElement(false);
	if (!mapNode) {
		return NULL;
	}
	std::string mapName = mapNode->Value();
	if (mapName.empty()) {
		return NULL;
	}
	Areas area = area_unknown;
	for (int i = 0; i < area_unknown; i++) {
		if (!STRCMP(AreaNames[i], mapName.c_str())) {
			area = (Areas)i;
			break;
		}
	}
	Options * o;
	Area * a;
	switch (area) {
		case philadelphia:
			o = new PhiladelphiaOptions(*base);
			o->Read(mapNode);
			a = new Philadelphia(o);
			break;
		case coords:
			o = new CoordinatesOptions(*base);
			o->Read(mapNode);
			a = new Coordinates(o);
			break;
		case area_unknown:
		default:
			return NULL;
	}
	o->Print();
	return a;
}
